package org.letva.toink.auditlog.interceptor;


import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import jodd.bean.BeanUtil;
import org.apache.log4j.Logger;
import org.hibernate.EmptyInterceptor;
import org.hibernate.Session;
import org.hibernate.type.Type;
import org.letva.toink.auditlog.annotation.AuditType;
import org.letva.toink.auditlog.annotation.NonAuditField;
import org.letva.toink.auditlog.annotation.NonAuditType;
import org.letva.toink.auditlog.model.AuditLog;
import org.letva.toink.model.ToinkEntity;
import org.letva.toink.util.HibernateUtil;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.sql.Timestamp;
import java.util.*;

public class SimpleAuditLogInterceptor extends EmptyInterceptor {

    private static final Logger log = Logger.getLogger(SimpleAuditLogInterceptor.class);

    private Set<ToinkEntity> deleted = new HashSet<ToinkEntity>();
    private Set<ToinkEntity> updated = new HashSet<ToinkEntity>();
    private Set<ToinkEntity> created = new HashSet<ToinkEntity>();

    private static final String CREATE_ACTION = "create";
    private static final String UPDATE_ACTION = "update";
    private static final String DELETE_ACTION = "delete";

    public SimpleAuditLogInterceptor() {
        log.info("SimpleAuditLogInterceptor created " + this);
    }

    @Override
    public void postFlush(Iterator entities) {
        super.postFlush(entities);

        for (ToinkEntity entity : deleted) {
            writeAuditLogEntry(DELETE_ACTION, entity);
        }

        for (ToinkEntity entity : updated) {
            writeAuditLogEntry(UPDATE_ACTION, entity);
        }

        for (ToinkEntity entity : created) {
            writeAuditLogEntry(CREATE_ACTION, entity);
        }

        deleted.clear();
        updated.clear();
        created.clear();
    }

    private void writeAuditLogEntry(String action, ToinkEntity entity) {
        Class entityClass = entity.getClass();

        // don't want to do the audit magic? ok.
        if (entityClass.isAnnotationPresent(NonAuditType.class)) {
            return;
        }

        // Go through all the children?
        if (entityClass.isAnnotationPresent(AuditType.class)) {
            AuditType auditType = (AuditType) entityClass.getAnnotation(AuditType.class);
            boolean isDeep = auditType.isDeepAudit();

            Map<String, Object> auditFields = new HashMap<String, Object>();
            while (entityClass != Object.class) {
                for (int i = 0; i < entityClass.getDeclaredFields().length; i++) {
                    Field field = entityClass.getDeclaredFields()[i];
                    Object fieldValue;
                    String fieldName;

                    if (!field.isAnnotationPresent(NonAuditField.class)) {
                        fieldName = field.getName();
                        fieldValue = BeanUtil.getPropertySilently(entity, fieldName);

                        if (fieldValue instanceof Collection || fieldValue instanceof Object[] || fieldValue instanceof ToinkEntity) {
                            continue; // don't audit collections and arrays
                        }

                        if (fieldValue instanceof Date) {
                            auditFields.put(fieldName, ((Date)fieldValue).getTime());
                        } else if (fieldValue != null) {
                            auditFields.put(fieldName, fieldValue);
                        }
                    }
                }

                if (isDeep) {
                    entityClass = entityClass.getSuperclass();
                } else {
                    break;
                }
            }
            log.debug("AuditLog Entry details: class name = " + entity.getClass().getName() + ", details = " + auditFields);
            AuditLog auditLog = createAuditLogEntry(action, entity, auditFields);

            log.info("AuditLog Entry created: " + auditLog);
        }
    }

    @Override
    public boolean onSave(java.lang.Object entity, java.io.Serializable id, java.lang.Object[] state, java.lang.String[] propertyNames, org.hibernate.type.Type[] types) {
        boolean ret = super.onSave(entity, id, state, propertyNames, types);

        if (entity instanceof ToinkEntity) {
            created.add((ToinkEntity) entity);
        }

        return ret;
    }


    @Override
    public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, org.hibernate.type.Type[] types) {
        boolean ret = super.onFlushDirty(entity, id, currentState, previousState, propertyNames, types);

        if (entity instanceof ToinkEntity) {
            updated.add((ToinkEntity) entity);
        }

        return ret;
    }

    @Override
    public void onDelete(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
        super.onDelete(entity, id, state, propertyNames, types);

        if (entity instanceof ToinkEntity) {
            deleted.add((ToinkEntity) entity);
        }
    }

    private AuditLog createAuditLogEntry(String action, ToinkEntity tEntity, Map auditFields) {
        AuditLog auditLog = new AuditLog();
        auditLog.setAction(action);

        Gson gson = new GsonBuilder().create();
        String details = gson.toJson(auditFields);
        auditLog.setDetail(details);

        auditLog.setEntityName(tEntity.getClass().getName());
        auditLog.setEntityId(tEntity.getId());
        auditLog.setCreatedDate(new Timestamp(new Date().getTime()));
        Session session = HibernateUtil.openSession(null);
        try {
            session.beginTransaction();
            session.saveOrUpdate(auditLog);
            session.getTransaction().commit();
            session.flush();
        } finally {
            session.close();
        }
        return auditLog;
    }

}

